package com.hqd.ch03.v19.aop.framework.aspectj;

import com.hqd.ch03.v19.aop.ClassFilter;
import com.hqd.ch03.v19.aop.ExpressionPointcut;
import com.hqd.ch03.v19.aop.MethodMatcher;
import com.hqd.ch03.v19.aop.ProxyMethodInvocation;
import com.hqd.ch03.v19.aop.interceptor.ExposeInvocationInterceptor;
import com.hqd.ch03.v19.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.weaver.tools.*;

import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

/**
 * 切点表达式匹配
 */
public class AspectJExpressionPointcut implements ClassFilter, MethodMatcher, ExpressionPointcut {
    private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>();


    static {
        /**
         * 定义 AspectJ 匹配器
         */
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.ARGS);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.REFERENCE);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.THIS);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.TARGET);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.WITHIN);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ANNOTATION);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_WITHIN);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ARGS);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_TARGET);
    }

    /**
     * 切点表达式
     */
    private String expression;
    private String[] pointcutParameterNames = new String[0];
    private Class<?>[] pointcutParameterTypes = new Class<?>[0];
    private PointcutExpression pointcutExpression;

    public AspectJExpressionPointcut(String expression) {
        this.expression = expression;
    }


    public PointcutExpression buildPointcutExpression() {
        if (this.pointcutExpression == null) {
            PointcutParser parser = initializePointcutParser(Thread.currentThread().getContextClassLoader());
            PointcutParameter[] pointcutParameters = new PointcutParameter[this.pointcutParameterNames.length];
            /**
             * 匹配对应的参数
             */
            for (int i = 0; i < pointcutParameters.length; i++) {
                pointcutParameters[i] = parser.createPointcutParameter(
                        this.pointcutParameterNames[i], this.pointcutParameterTypes[i]);
            }
            this.pointcutExpression = parser.parsePointcutExpression(replaceBooleanOperators(getExpression()),
                    null, pointcutParameters);
        }
        return this.pointcutExpression;
    }

    /**
     * 初始化切点匹配器
     *
     * @param classLoader
     * @return
     */
    private PointcutParser initializePointcutParser(@Nullable ClassLoader classLoader) {
        PointcutParser parser = PointcutParser
                .getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(
                        SUPPORTED_PRIMITIVES, classLoader);
        return parser;
    }

    public String getExpression() {
        return this.expression;
    }

    @Override
    public boolean matches(Class<?> clazz) {
        PointcutExpression pointcutExpression = buildPointcutExpression();
        return pointcutExpression.couldMatchJoinPointsInType(clazz);
    }

    @Override
    public ClassFilter getClassFilter() {
        return this;
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        return this;
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        /**
         * TODO 这个方法获取非常麻烦，需要判断是否接口方法、桥接方法等
         * 这里先直接获取
         */
        return matches(method, targetClass, new Object[]{});
    }

    @Override
    public boolean isRuntime() {
        return buildPointcutExpression().mayNeedDynamicTest();
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        PointcutExpression pointcutExpression = buildPointcutExpression();
        ShadowMatch shadowMatch = pointcutExpression.matchesMethodExecution(method);
        ProxyMethodInvocation pmi = null;
        Object targetObject = null;
        Object thisObject = null;
        try {
            MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation();
            targetObject = mi.getThis();
            pmi = (ProxyMethodInvocation) mi;
            thisObject = pmi.getProxy();
        } catch (IllegalStateException ex) {
            throw new RuntimeException(ex);
        }
        JoinPointMatch joinPointMatch = shadowMatch.matchesJoinPoint(thisObject, targetObject, args);
        if (pmi != null && thisObject != null) {
            /**
             * 绑定到调用链中
             */
            if (joinPointMatch.matches()) {
                bindParameters(pmi, joinPointMatch);
            }
        }
        return joinPointMatch.matches();
    }

    private String replaceBooleanOperators(String pcExpr) {
        String result = StringUtils.replace(pcExpr, " and ", " && ");
        result = StringUtils.replace(result, " or ", " || ");
        return StringUtils.replace(result, " not ", " ! ");
    }

    private void bindParameters(ProxyMethodInvocation invocation, JoinPointMatch jpm) {
        invocation.setUserAttribute(this.expression, jpm);
    }

    public void setPointcutParameterNames(String... pointcutParameterNames) {
        this.pointcutParameterNames = pointcutParameterNames;
    }

    public void setPointcutParameterTypes(Class<?>... pointcutParameterTypes) {
        this.pointcutParameterTypes = pointcutParameterTypes;
    }

}
